package com.imist.nettybasic2;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.AttributeKey;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;

import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * 客户端的启动
 */
public class NettyClient {
    private static final int MAX_RETRY = 10;
    private static final String host = "127.0.0.1";
    private static final int port = 8000;

    /**
     * 服务端启动，需要设置 childxxx 相关的属性，是因为服务端启动不仅仅监听本地端口，还一直与客户端的连接进行交互，
     * 可以理解为有两种类型的 channel,对于单纯的客户端代码来讲，可以理解为只有workgroup，因为只需要连接，没有监听端口的
     * 逻辑
     * @param args
     */
    public static void main(String[] args) {
        Bootstrap bootstrap = new Bootstrap();
        NioEventLoopGroup group = new NioEventLoopGroup();
        bootstrap
                //1. 指定线程模型
                .group(group)
                // 2. 指定IO模型为NIO
                .channel(NioSocketChannel.class)
                //可以给客户端 Channel，也就是NioSocketChannel绑定自定义属性，然后我们可以通过channel.attr()取出这个属
                .attr(AttributeKey.newInstance("clientName"),"nettyClient")

        /**
         * ChannelOption.CONNECT_TIMEOUT_MILLIS 表示连接的超时时间，超过这个时间还是建立不上的话则代表连接失败
         * ChannelOption.SO_KEEPALIVE 表示是否开启 TCP 底层心跳机制，true 为开启
         * ChannelOption.TCP_NODELAY 表示是否开始 Nagle 算法，true 表示关闭，false 表示开启，通俗地说， 如果要求
         * 高实时性，有数据发送时就马上发送，就设置为 true 关闭，如果需要减少发送次数减少网络交互，就设置为 false 开启*/

                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS,5000)
                .option(ChannelOption.SO_KEEPALIVE,true)
                .option(ChannelOption.TCP_NODELAY,true)
                // 3.IO处理逻辑
                .handler(new ChannelInitializer<Channel>() {
                    @Override
                    protected void initChannel(Channel channel) throws Exception {
                        channel.pipeline().addLast(new StringEncoder());
                    }
                });
        //4. 建立连接
         ChannelFuture channelFuture = bootstrap.connect(host,port);
         //监听连接结果，addListener通常是在一个一个具体的操作，比如bind，write，read之后添加，表示监听该操作是否成功
         channelFuture .addListener(future ->{
                    if (future.isSuccess()){
                        System.out.println("连接成功!");
                    }else {
                        System.out.println("连接失败!");
                        //重新连接
                        connect(bootstrap,host,port,MAX_RETRY);
                    }
                 });
    }


    /**
     * 通常情况下，连接建立失败不会立即重新连接，而是会通过一个指数退避的方式，比如每隔 1 秒、2 秒、4 秒、8 秒，
     * 以 2 的幂次来建立连接，然后到达一定次数之后就放弃连接，接下来我们就来实现一下这段逻辑，我们默认重试 5 次
     */
    private static void  connect(Bootstrap bootstrap,String host,int port,int retry){
        bootstrap.connect(host,port).addListener(new GenericFutureListener<Future<? super Void>>() {
            @Override
            public void operationComplete(Future<? super Void> future) throws Exception {
                if (future.isSuccess()){
                    System.out.println("连接成功！");
                }else if(retry == 0){
                    System.out.println("重连次数已用完，放弃连接");
                }else {
                   //第几次重连
                    int order = (MAX_RETRY - retry) + 1;
                    // 本次重连的间隔
                    int delay = 1 << order;
                    System.out.println(new Date() +": 连接失败，第 "+order+"次重连....");
                    /**\
                     * 定时任务是调用 bootstrap.config().group().schedule(), 其中 bootstrap.config() 这个方法返回的是 BootstrapConfig，他是对 Bootstrap 配置参数的抽象，
                     * 然后 bootstrap.config().group() 返回的就是我们在一开始的时候配置的线程模型 workerGroup，调 workerGroup 的 schedule 方法即可实现定时任务逻辑。
                     */
                    bootstrap.config().group().schedule(()  ->
                            connect(bootstrap,host,port,retry -1),delay, TimeUnit.SECONDS );
                }
            }
        });
    }

}
